package NG::Chat::Command;
use strict;
use warnings;
use utf8;

#use NG::Chat::Util;
use NG::Sys::Datetime;
use NG::Sys::Text;
use JSON::XS;
use EV;

our %command = (
	unreg =>    [\&cmd_unreg,        1,1], # $msg->{userlogin}
	reg =>      [\&cmd_reg,          1,1], # $msg->{userlogin},$msg->{userpass},$msg->{tmp},$msg->{chanel}
	chlog =>    [\&cmd_chlog,        1,1], # $msg->{userlogin},$msg->{newlogin},$msg->{chanel}
	chpass =>   [\&cmd_setpass,      1,1], # $msg->{userlogin},$msg->{userpass}
	chchanel => [\&cmd_setchanel,    1,1], # $msg->{userlogin},$msg->{chanel}
	login =>    [\&cmd_login,],            # $msg->{login},$msg->{pass}
	check =>    [\&cmd_check,        1],   # 
	message =>  [\&cmd_message,      1],   # $msg->{chanel},$msg->{text}
	private =>  [\&cmd_private,      1],   # $msg->{to},$msg->{text}
	list =>     [\&cmd_list_chanel,  1],   # $msg->{chanel}
	join =>     [\&cmd_join_chanel,  1,1], # $msg->{chanel},$msg->{userlogin}
	leave =>    [\&cmd_leave_chanel, 1,1], # $msg->{chanel},$msg->{userlogin}
	create =>   [\&cmd_create_chanel,1,1], # $msg->{chanel},$msg->{param}
	remove =>   [\&cmd_remove_chanel,1,1], # $msg->{chanel}
	ping =>     [\&cmd_ping,         1],   #
	command =>  [\&cmd_command,      1,1],
	cast =>     [\&cmd_cast,         1,1],
	echo =>     [\&cmd_echo,         1,1], # $msg->{text}
	info =>     [\&cmd_info,         1,1], # $msg->{userlogin}
);



sub Create {
	my $app = shift;
	my %obj = (app => $app);
	# список клиентов по логинам
	$obj{waiting} = {};
	$obj{logger} = $app->make_syslog($app->{chatlog});

	$obj{BroadCast} = {'1'=>{type=>"message",from=>"server",text=>"server started",ts=>unixtime2hms(),ats=>time()}};
	$obj{iBroadCastHas} = 1;
	$obj{iBroadCast} = 1;
	$obj{Topic} = {'1'=>{type=>"notice",param2=>"server started ".unixtime2datetime(),param1=>"topic",ats=>time()}};
	$obj{iTopic} = 1;
	$obj{command} = \%command;
	$obj{journal} = [];
	$obj{ijournal} = 0;
	$obj{mjournal} = $app->{journal_max} || 10000;
	$obj{djournal} = $app->{journal_del} || 1000;

	my $self = bless \%obj,__PACKAGE__;

	# принимает сообщение, обрабатывает и отдает результат
	# что далать с результатом решает тот кто запросил обработку
	$obj{process} = sub {
		my ($msg,$fh,$sender) = @_;
		my $ret;
		if(my $handler = $self->{command}{$msg->{cmd} || ""}){
			# требуется авторизация
			if($handler->[1]){
				if(my $login = $self->{app}{'chat-login'}->check_session($msg)){
					$msg->{login} = $login;
					# доступно только админу
					if($handler->[2] && $login !~ /^admin/){
						return  +{
							command => $msg->{cmd},
							error => 403,
							text => "not permited",
						};
					}
				}else{
					return  +{
						command => $msg->{cmd},
						error => 401,
						text => "need authorisation",
					};
				}
			}
			# выполняем
			if($ret = $handler->[0]($self,$msg,$fh,$sender)){
				unless($ret->{nowait}){
					$ret->{rid} ||= $msg->{rid};
					$ret->{command} ||= $msg->{cmd};
				}else{
					$ret = undef;
				}
			}
		}else{
			$ret = {
				command => $msg->{cmd},
				error => 405,
				text => "invalid command [$msg->{cmd}]",
			};
		}
		return $ret;
	};

	return $self;
}
sub cleanup {
	my $self = shift;
	delete $self->{app};
	delete $self->{process};
	delete $self->{command};
	delete $self->{waiting};
	delete $self->{logger};
	delete $self->{BroadCast};
	delete $self->{Topic};
}

# обработчики команд
sub cmd_reg {
	my ($self,$msg) = @_;
	if(my $err = $self->{app}{'chat-login'}->create_login($msg->{userlogin},$msg->{userpass},$msg->{tmp},$msg->{chanel})){
		return $err;
	}else{
		return {text=>"ok [$msg->{userlogin}] $msg->{userpass}"};
	}
}
sub cmd_unreg {
	my ($self,$msg) = @_;
	if(my $err = $self->{app}{'chat-login'}->remove_login($msg->{userlogin})){
		return $err;
	}else{
		return {text=>"ok [$msg->{userlogin}]"};
	}
}
sub cmd_chlog {
	my ($self,$msg) = @_;
	if(my $err = $self->{app}{'chat-login'}->change_login($msg->{userlogin},$msg->{newlogin},,$msg->{chanel})){
		return $err;
	}else{
		return {text=>"ok [$msg->{userlogin}]  to [$msg->{newlogin}]"};
	}
}
sub cmd_setpass {
	my ($self,$msg) = @_;
	if(my $err = $self->{app}{'chat-login'}->change_pass($msg->{userlogin},$msg->{userpass})){
		return $err;
	}else{
		return {text=>"ok"};
	}
}
sub cmd_login {
	my ($self,$msg,$fh,$sender) = @_;
	return $self->{app}{'chat-login'}->enter_login($msg->{login},$msg->{pass},$fh,$sender);
}
sub cmd_ping {
	return {text=>"ok"};
}
sub cmd_echo {
	my $self = shift;
	my $msg = shift;
	return {text=>$msg->{text}};
}
sub cmd_info {
	my $self = shift;
	my $msg = shift;
	return $self->{app}{'chat-login'}->info_login($msg->{userlogin});
}
sub cmd_message {
	my ($self,$msg) = @_;
	if(my $err = $self->{app}{'chat-chanel'}->can_message($msg->{chanel},$msg->{login})){
		return $err;
	}elsif(my $diff = $self->{app}{'chat-login'}->check_wipe($msg->{login})){
			return {error=>408,text=>"try over $diff sec",};
	}else{
		my $text = strip_html($msg->{text});
		$self->{logger}->send("<".$msg->{chanel}."> [".$msg->{login}."] ".$text);
		my $mid = reg_message($self,0,$msg->{login},$text);
		put_message($self,{type=>"message",mid => $mid,rid=>$msg->{rid},title=>$msg->{title},from=>$msg->{login},text=>$text,chanel=>$msg->{chanel}});
		return {text=>"ok"};
	}
}
sub cmd_private {
	my ($self,$msg) = @_;
	unless($self->{app}{'chat-login'}->can_private($msg->{login})){
		return cmd_message($self,$msg);
	}elsif(my $diff = $self->{app}{'chat-login'}->check_wipe($msg->{login})){
		return {error=>408,text=>"try over $diff sec",};
	}else{
		my $text = strip_html($msg->{text});
		$self->{logger}->send("<private> [".$msg->{login}."] ".$text);
		my $mid = reg_message($self,1,$msg->{login},$text);
		push @{$msg->{to}},$msg->{login};
		put_private($self,{type=>"message",mid=>$mid,rid=>$msg->{rid}, title=>$msg->{title}, from=>$msg->{login}, text=>$text,to => $msg->{to}, private => 1});
		return {text=>"ok",};
	}
}
sub cmd_command {
	my ($self,$msg) = @_;

	$self->{logger}->send("<command> to [".$msg->{to}."] ".$msg->{param1}.":".encode_json($msg->{param2}||[]));
	put_private($self,{
					type => "notice",
					param1 => "exec",
					param2 => $msg->{param1},
					param3 => $msg->{param2},
					to => [ $msg->{to} ],
					});
	return {text=>"ok",};
}
# вот здесь нужно ждать
sub cmd_check {
	my ($self,$msg,$fh,$sender) = @_;
	# получаем список сообщений
	my $list = build_list_message($self,$msg->{login});
	#  если есть ли новые сообщения
	if(@$list){
		return {type=>"list",list=>$list};
	}else{
		# ничего не возвращаем, потому что нету
		# както надо указать, что здесь мы ничего не возвращаем
		# не потому что просто игнорим,
		# а потому что нужно подождать ответа
		my $to = $self->{app}{check_timeout} || 55;
		
		my $w = EV::timer $to, 0, \&check_timeout;
		# опасно, но делать нечего, придется оставлять ссылку на хэш вейтингов, для строки 230
		$w->data([$fh,$sender,$msg->{rid},$msg->{login},$self->{waiting}]);
		$self->{waiting}{$msg->{login}} = $w;
		return undef;
	}
}
sub check_timeout {
	my $w = shift;
	my $target = $w->data("");
	# вот здесь удаляем эту ссылку
	delete $target->[4]{$target->[3]};
	$target->[1]->($target->[0],{type=>"list",list=>[],rid=>$target->[2]});
}
sub cmd_list_chanel {
	my ($self,$msg) = @_;
	unless(my $err = $self->{app}{'chat-chanel'}->can_list($msg->{chanel},$msg->{login})){
		if(my $list = $self->{app}{'chat-chanel'}->list_logins($msg->{login},$msg->{chanel})){
			return {type=>"notice",chanel=>$msg->{chanel},param1=>"list",param2=>$list};
		}else{
			return {chanel=>$msg->{chanel},error=>404,text=>"bad chanel",};
		}
	}else{
		return $err;
	}
}
sub cmd_join_chanel {
	my ($self,$msg) = @_;
	unless(my $err = $self->{app}{'chat-chanel'}->can_join($msg->{chanel},$msg->{userlogin})){
		if(my $title = $self->{app}{'chat-chanel'}->join_login($msg->{userlogin},$msg->{chanel})){
			return {chanel=>$msg->{chanel},text=>"ok",chaneltitle=>$title};
		}else{
			return {chanel=>$msg->{chanel},error=>404,text=>"bad chanel"};
		}
	}else{
		return $err;
	}
}
sub cmd_leave_chanel {
	my ($self,$msg) = @_;
	unless(my $err = $self->{app}{'chat-chanel'}->can_split($msg->{chanel},$msg->{userlogin})){
		if($self->{app}{'chat-chanel'}->split_login($msg->{userlogin},$msg->{chanel})){
			return {chanel=>$msg->{chanel},text=>"ok",};
		}else{
			return {chanel=>$msg->{chanel},error=>404,text=>"bad chanel",};
		}
	}else{
		return $err;
	}
}
sub cmd_create_chanel {
	my ($self,$msg) = @_;
	unless(my $err = $self->{app}{'chat-chanel'}->create_chanel($msg->{chanel},$msg->{chaneltitle},$msg->{param})){
		return {text=>"ok",};
	}else{
		return $err;
	}
}
sub cmd_remove_chanel {
	my ($self,$msg) = @_;
	unless(my $err = $self->{app}{'chat-chanel'}->remove_chanel($msg->{chanel})){
		return {text=>"ok",};
	}else{
		return $err;
	}
}
sub cmd_cast {
	my ($self,$msg) = @_;
	if($self->{iBroadCast} - $self->{iBroadCastHas} > 100){
		delete @{$self->{BroadCast}}{($self->{iBroadCastHas} .. ($self->{iBroadCastHas}+50))};
		$self->{iBroadCastHas} += 50;
	}
	my $ret = {
		ats=>time()
	};
	if($msg->{param1} eq "message"){
		$ret->{type} = "message";
		$ret->{from} = $msg->{param2};
		$ret->{text} = $msg->{param3};
		#$ret->{ts} = unixtime2hms();
	}
	elsif($msg->{param1} eq "exec"){
		$ret->{type} = "notice";
		$ret->{param1} = "exec";
		$ret->{param2} = $msg->{param2};
	}
	$self->{BroadCast}{++$self->{iBroadCast}} = $ret;
	return {text=>"ok"};
}
#########################################
sub put_private {
	my ($self,$msg) = @_;
	#$msg->{ts} = unixtime2hms();
	$msg->{ats} = time();
	my %sended;
	for my $to(@{$msg->{to}}){
		next if exists $sended{$to};
		# сохраняем сообщение, если адресат существует
		if(my $l = $self->{app}{'chat-login'}{logins}{$to}){
			push @{$l->{private}},$msg;
			# если есть в списке ожидающих, то отправляем сразу
			if(my $wt = delete $self->{waiting}{$to}){
				my $target = $wt->data("");
				my $list;
				if(@{$list = build_list_message($self,$to)}){
					# $target->[1] = sender(fh,msg)
					# $target->[0] = fh
					# $target->[2] = rid for last "check"
					$target->[1]->($target->[0],{type=>"list",list=>$list,rid=>$target->[2]});
				}
			}
		}else{
			$self->{logger}->send("private sended to $to, but not exists");
		}
	}
}
# рассылка входящего сообщения
sub put_message {
	my ($self,$msg) = @_;
	my $chanel = $self->{app}{'chat-chanel'}->get_chanel($msg->{chanel});
	unless($chanel){
		return;
	}
	if($chanel->{close}){
		return;
	}
	#$msg->{ts} = unixtime2hms();
	$msg->{ats} = time();
	# так как отдаем не больше 100 последних, то хранить больше не имеет смысл
	push @{$chanel->{messages}},$msg;
	if(@{$chanel->{messages}} > 500){
		splice @{$chanel->{messages}},0,350;
		$chanel->{shift} += 350;
	}
	$chanel->{last} = time();
	# пройти по всем ожидающим и отправить
	#my @wdone;
	for my $login(keys %{$chanel->{list}}){
		if(my $wt = delete $self->{waiting}{$login}){
			my $target = $wt->data("");
			my $list;
			if(@{$list = build_list_message($self,$login)}){
				$target->[1]->($target->[0],{type=>"list",list=>$list,rid=>$target->[2]});
			}
		}
	}
}
sub build_list_message {
	my ($self,$login) = @_;
	unless($login){
		# если сессии уже нет, то ничего не делаем
		return [];
	}
	my @list;

	my $lgn = $self->{app}{'chat-login'}{logins}{$login};
	my $lt;
	unless(($lt = $lgn->{topic}) && ($lt >= $self->{iTopic})){
		$lgn->{topic} = $self->{iTopic};
		push @list, $self->{Topic}{$self->{iTopic}};
	}
	unless(($lt = $lgn->{bc}||0) && ($lt >= $self->{iBroadCast})){
		$lt++;
		$lgn->{bc} = $self->{iBroadCast};
		push @list, @{$self->{BroadCast}}{($lt .. $self->{iBroadCast})};
	}
	if(@{$lgn->{private}}){
		push @list,@{$lgn->{private}};
		$lgn->{private} = [];
	}
	for my $id(keys %{$lgn->{chanels}}){
		my $ch = $self->{app}{'chat-chanel'}->get_chanel($id);
		next unless $ch;

		my $last = $lgn->{chanels}{$id};
		my $messages = $ch->{messages};
		# отсылаем только последние 100 сообщений
		if(@$messages - ($last -= $ch->{shift}) > 100){
			$last = @$messages - 100;
		}
		if($last < @$messages){
			push @list,@{$messages}[$last .. $#$messages];
			$lgn->{chanels}{$id} = $ch->{shift} + @$messages;
		}
	}
	return \@list;
}
sub reg_message {
	my $self = shift;
	my $type = shift;
	my $from = shift;
	my $msg = shift;
	my $ary;
	push @{$ary = $self->{journal}},[$type,$from,$msg];
	if(@$ary > $self->{mjournal}){
		splice @$ary,0,$self->{djournal};
		$self->{ijournal} += $self->{djournal};
	}
	return @$ary + $self->{ijournal};
}
1;
